﻿using Assimp;
using Core.Maths;
using Core.Rendering.Geometry;
using OpenTK.Mathematics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;

namespace Core.Rendering.Resources
{
    public class Model : IDisposable
    {
        public string? Path { get; }
        public List<Mesh> Meshes { get; } = [];
        public List<string> MaterialName { get; } = [];
        public Sphere BoundingSphere { get; private set; }
        public bool IsDestroy { get; private set; } = false;
        public static Model Create(string path,PostProcessSteps postProcessSteps = PostProcessSteps.Triangulate) => new Model(path,postProcessSteps);
        private Model(string path,PostProcessSteps postProcessSteps)
        {
            Path = path;
            (Meshes, MaterialName) = LoadModel(path,postProcessSteps);
            CreateBoundingSphere(Meshes);
        }
        private void CreateBoundingSphere(List<Mesh> meshes)
        {
            if(meshes.Count == 0)
            {
                return;
            }
            if(meshes.Count == 1)
            {
                BoundingSphere = meshes[0].BoundingSphere;
                return;
            }
            float minX = float.MaxValue;
            float minY = float.MaxValue;
            float minZ = float.MinValue;
            float maxX = float.MinValue;
            float maxY = float.MinValue;
            float maxZ = float.MaxValue;
            foreach (Mesh mesh in meshes)
            {
                minX = MathHelper.Min(minX, mesh.BoundingSphere.Position.X - mesh.BoundingSphere.Radius);
                minY = MathHelper.Min(minY, mesh.BoundingSphere.Position.Y - mesh.BoundingSphere.Radius);
                minZ = MathHelper.Min(minZ, mesh.BoundingSphere.Position.Z - mesh.BoundingSphere.Radius);
                maxX = MathHelper.Max(minX, mesh.BoundingSphere.Position.X + mesh.BoundingSphere.Radius);
                maxY = MathHelper.Max(minY, mesh.BoundingSphere.Position.Y + mesh.BoundingSphere.Radius);
                maxZ = MathHelper.Max(minZ, mesh.BoundingSphere.Position.Z + mesh.BoundingSphere.Radius);
            }
            Vector3 position = new Vector3(minX + maxX, minY + maxY, minZ + maxZ) / 2;
            float radius = MathHelper.Max(
                Vector3.Distance(position, new Vector3(minX, minY, minZ)), 
                Vector3.Distance(position, new Vector3(maxX, maxY, maxZ)));
            BoundingSphere = new Sphere(position, radius);
        }
        public static (List<Mesh> meshes, List<string> materialNames) LoadModel(string? path, PostProcessSteps postProcessSteps = PostProcessSteps.None)
        {
            AssimpContext assimp = new AssimpContext();
            Scene scene = assimp.ImportFile(path, postProcessSteps);
            if (scene is null || (scene.SceneFlags & SceneFlags.Incomplete) == SceneFlags.Incomplete || scene?.RootNode is null)
            {
                Console.WriteLine($"ERROR:ASSIMP");
                return (null, null);
            }
            var materialNames = ProcessMaterials(scene);
            List<Mesh> meshes = new List<Mesh>();
            ProcessNode(scene, meshes, scene.RootNode, Matrix4x4.Identity);
            return (meshes, materialNames);
        }
        private static List<string> ProcessMaterials(Scene scene)
        {
            List<string> materialNames = new List<string>();
            if (scene.HasMaterials)
            {
                foreach (var mat in scene.Materials)
                {
                    materialNames.Add(mat.HasName ? mat.Name : "Default");
                }
            }
            return materialNames;
        }
        private static void ProcessNode(Scene scene, List<Mesh> meshes, Node rootNode, Matrix4x4 transform)
        {
            foreach (var index in rootNode.MeshIndices)
            {
                Assimp.Mesh mesh = scene.Meshes[index];
                meshes.Add(ProcessMesh(mesh, transform));
            }
            foreach (var node in rootNode.Children)
            {
                ProcessNode(scene, meshes, node, rootNode.Transform * node.Transform);
            }
        }
        private static Mesh ProcessMesh(Assimp.Mesh mesh, Matrix4x4 transform)
        {
            List<Vertex> vertices = new List<Vertex>();
            List<uint> indices = new List<uint>();
            int materialIndex = mesh.MaterialIndex;
            string meshName = mesh.Name;
            for (int i = 0; i < mesh.VertexCount; i++)
            {
                var position = transform * (mesh.HasVertices ? mesh.Vertices[i] : new Vector3D(0, 0, 0));
                var normal = transform * (mesh.HasNormals ? mesh.Normals[i] : new Vector3D(0, 0, 0));
                var texCoords = mesh.HasTextureCoords(0) ? mesh.TextureCoordinateChannels[0][i] : new Vector3D(0, 0, 0);
                var tangent = transform * (mesh.HasTangentBasis ? mesh.Tangents[i] : new Vector3D(0, 0, 0));
                var biTangent = transform * (mesh.HasTangentBasis ? mesh.BiTangents[i] : new Vector3D(0, 0, 0));
                Vertex vertex = new Vertex();
                vertex.Position.X = position.X;
                vertex.Position.Y = position.Y;
                vertex.Position.Z = position.Z;
                vertex.TexCoords.X = texCoords.X;
                vertex.TexCoords.Y = texCoords.Y;
                vertex.Normal.X = normal.X;
                vertex.Normal.Y = normal.Y;
                vertex.Normal.Z = normal.Z;
                vertex.Tangent.X = tangent.X;
                vertex.Tangent.Y = tangent.Y;
                vertex.Tangent.Z = tangent.Z;
                vertex.BiTangent.X = biTangent.X;
                vertex.BiTangent.Y = biTangent.Y;
                vertex.BiTangent.Z = biTangent.Z;
                vertices.Add(vertex);
            }
            foreach( var index in mesh.GetIndices())
            {
                indices.Add((uint)index);
            }
            //if (mesh.HasFaces)
            //{
            //    foreach (var face in mesh.Faces)
            //    {
            //        foreach (var faceIndex in face.Indices)
            //        {
            //            indices.Add((uint)faceIndex);
            //        }
            //    }
            //}
            return new Mesh(vertices, indices, materialIndex, meshName);
        }
        public void Dispose()
        {
            if (IsDestroy) return;
            IsDestroy = true;
            foreach (Mesh mesh in Meshes)
            {
                mesh.Dispose();
            }
        }
    }
}
